package org.gienah;

import java.lang.reflect.Method;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.gienah.internal.CacheProvider;
import org.gienah.internal.Injector;
import org.junit.internal.runners.InitializationError;
import org.junit.internal.runners.JUnit4ClassRunner;
import org.junit.internal.runners.MethodRoadie;
import org.junit.internal.runners.TestClass;
import org.junit.runner.Description;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunNotifier;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

/**
 * A <code>SpringRunner</code> runs tests and provides access to defined
 * Spring related features like bean injection and transactional tests. To use
 * this runner, you need to mark your test class with the <code>RunWith</code>
 * annotation. Besides, you must to specify the Spring configuration using the
 * <code>Configuration</code> annotation.
 * 
 * @see org.gienah.Configuration
 * @see org.junit.internal.runners.TestClassRunner
 * @see org.junit.runner.Runner
 * @see org.junit.runner.RunWith
 * @author Santiago L. Valdarrama
 * @since E0.1
 */
public class SpringRunner
		extends JUnit4ClassRunner {

	private static final Log logger = LogFactory.getLog(SpringRunner.class);

	@Dependency
	private PlatformTransactionManager transactionManager;
	private TransactionStatus transactionStatus;

	private RunNotifier notifier;

	/**
	 * Constructs a new instance of <code>SpringRunner</code> class.
	 * 
	 * @param clazz The class containing the tests to be run.
	 * @throws InitializationError Thrown if anything wrong happen during runner
	 *         construction.
	 */
	public SpringRunner(Class<?> clazz)
			throws InitializationError {

		super(clazz);
	}

	public void initializeTransaction() {
		if (this.transactionManager != null) {
			if (this.transactionStatus == null) {
				this.transactionStatus = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
				if (SpringRunner.logger.isDebugEnabled()) {
					SpringRunner.logger.debug("Began transaction. Transaction manager [" + this.transactionManager + "]");
				}
			}
			else {
				SpringRunner.logger.warn("There is an existing unclosed transaction. Tests will not run within a transaction.");
			}
		}
		else {
			SpringRunner.logger.warn("The transaction manager is not set. Tests will not run within a transaction.");
		}
	}

	public void finalizeTransaction() {
		if (this.transactionStatus != null && !this.transactionStatus.isCompleted()) {
			try {
				this.transactionManager.rollback(this.transactionStatus);
				if (SpringRunner.logger.isDebugEnabled()) {
					SpringRunner.logger.debug("Rolled back transaction after test execution");
				}
			}
			finally {
				this.transactionStatus = null;
			}
		}
	}

	/**
	 * This method is responsible of injecting defined dependencies and running
	 * the tests for this runner.
	 * 
	 * @param notifier will be notified of events while tests are being run,
	 *        tests being started, finishing, and failing
	 */
	@Override
	public void run(RunNotifier notifier) {
		this.notifier = notifier;

		try {
			injectDependencies(notifier);
		}
		catch (Throwable t) {
			this.notifier.fireTestFailure(new Failure(getDescription(), t));
		}

		super.run(notifier);
	}

	@Override
	protected Object createTest()
			throws Exception {

		Object test = super.createTest();

		try {
			Injector.inject(test, test.getClass(), CacheProvider.getContext(test.getClass()));
		}
		catch (Throwable t) {
			this.notifier.fireTestFailure(new Failure(getDescription(), t));
		}

		return test;
	}

	private void injectDependencies(RunNotifier notifier)
			throws Exception {

		ConfigurableApplicationContext context = CacheProvider.getContext(getTestClass().getJavaClass());
		
		Class<?> currentClass = getClass();
		do {
			try {
				Injector.inject(this, currentClass, context);
			}
			catch (Throwable t) {
				notifier.fireTestFailure(new Failure(getDescription(), t));
			}
			currentClass = currentClass.getSuperclass();
		}
		while (!currentClass.equals(SpringRunner.class.getSuperclass()));

		Injector.inject(null, getTestClass().getJavaClass(), context);
	}

	@Override
	protected MethodRoadie createMethodRunner(Object test, Method method, RunNotifier notifier) {
		boolean transactional = getTestClass().getJavaClass().getAnnotation(Transactional.class) != null;
		if (!transactional) {
			transactional = method.getAnnotation(Transactional.class) != null;
		}
		
		return new SpringMethodRoadie(test, method, notifier, getDescription(), getTestClass(), transactional);
	}

	private class SpringMethodRoadie
			extends MethodRoadie {

		private final boolean transactional;

		public SpringMethodRoadie(Object test,
				Method method,
				RunNotifier notifier,
				Description description,
				TestClass testClass,
				boolean transactional) {

			super(test, method, notifier, description, testClass);
			this.transactional = transactional;
		}

		@Override
		public void runTest() {
			if (this.transactional) {
				initializeTransaction();
			}

			try {
				super.runTest();
			}
			finally {
				if (this.transactional) {
					finalizeTransaction();
				}
			}
		}

	}

}
